05 - 采样与数据管道
前置:04 篇的内容采集档位 —— 记不记 prompt 直接决定本篇所有数字。
本篇回答:这些数据一天几个 T,怎么少存又不丢关键信息。以及为什么传统 APM 那套采样配置搬到 Agent 上会失效。
本篇会用到的词:
| 词 | 意思 |
|---|---|
| 头采样(head sampling) | 在 trace 刚开始时就决定采不采。快、省内存,但那时你还不知道这次执行会不会出错 |
| 尾采样(tail sampling) | 等一棵 trace 的 span 收得差不多了再决定。能按结果筛选,代价是要把 span 攒在内存里 |
| Collector | OpenTelemetry 的数据管道进程。收 → 处理 → 转发,是做采样、脱敏、归一的地方 |
| OTLP | OpenTelemetry 的传输协议。SDK 发给 Collector、Collector 发给后端,走的都是它 |
| 基数(cardinality) | 一个属性有多少种不同取值。user_id 基数百万,model 基数几十 —— 高基数属性做成指标标签会把时序库打爆 |
| span metrics | 从 span 里现算出来的指标。它在采样之前算,所以采样丢掉的 trace 仍然计入统计 |
一、Agent trace 有三个反常特征
先把体积算清楚,因为后面所有决策都建立在这个数字上。
1.1 一天到底多少数据
假设:日均 1,000 万次 Agent 执行,每次平均 8 个 span,其中 3 个是模型调用。
| 内容采集档位(04 篇) | 单 span 均值 | 每次执行 | 日增量 | 30 天留存 |
|---|---|---|---|---|
| 档位 1:不记 prompt | 0.6 KB | 4.8 KB | 48 GB | 1.4 TB |
| 档位 2:记在 span 属性上 | 8 KB | 64 KB | 640 GB | 19 TB |
| 档位 2 + 长上下文(32K prompt) | 45 KB | 360 KB | 3.6 TB | 108 TB |
(按 1 token ≈ 3 字节 UTF-8 中文估算;实际值随业务差异很大,重点是量级不是精度)
记不记 prompt,差 13 倍;长上下文场景差 75 倍。 这就是 04 篇第三档"存到外部存储、span 上只留引用"的经济动机 —— 对象存储的单位成本比 OLAP 数据库低一到两个数量级,而 prompt 是"偶尔要看一眼、从不用来做聚合查询"的典型冷数据。
二、头采样在 Agent 上没什么用
传统做法是在 SDK 侧配一个比例:
# ❌ 这个配置在 Agent 场景下几乎等于关掉了排查能力
# 问题在于「决定采不采」发生在根 span 创建的那一刻,
# 而那时这次执行有没有出错、绕了几步、花了多少钱,全都还不知道
sampler = TraceIdRatioBased(0.1) # 只留 10%
传统服务里这么做还行,因为请求高度同质 —— 随机留 10% 得到的是一个有代表性的样本。Agent 不是:
| 特征 | 后果 |
|---|---|
| 失败率低但失败很贵 | 1% 的失败请求里,10% 采样后只剩 0.1%,样本量不够定位 |
| 长尾极重 | 你要查的永远是那条 90 秒的、绕了 40 步的执行,而它被随机丢掉的概率和别的一样 |
| 成本分布极不均匀 | 少数请求消耗大部分 token,采样后的成本统计与真实账单对不上 |
头采样唯一还成立的用法,是给 Collector 做限流兜底 —— 防止某个服务突然发疯把管道打爆,而不是作为常规策略。
三、尾采样的三个前提,Agent 打破了两个
尾采样处理器(tailsamplingprocessor,beta 阶段)的做法是把 span 攒在内存里,等一段时间后按策略判断。它有三个隐含前提。
3.1 前提一:trace 在 decision_wait 内结束
processors:
tail_sampling:
decision_wait: 30s # ← 默认值
num_traces: 50000 # ← 默认值,内存里最多攒这么多棵树
默认等 30 秒。 一次带工具调用的 Agent 执行经常超过这个数,01 篇开头那个例子就是 18.4 秒 —— 已经接近了。
超时会怎样?官方文档里"Late-Arriving Spans"那一节写得很清楚:
Late spans can cause different sampling decisions for different parts of the trace.
一棵树被劈成两半,前半段留下、后半段丢掉。 这比整棵丢更糟 —— 你在后台看到一棵结构完整的 trace,但它缺了最后那几个 span,而"最后那几个 span"往往正是出错的地方。
调大 decision_wait 又会撞上前提二。
3.2 前提二:内存装得下
num_traces 默认 50,000,实现是一个环形缓冲区:新的 trace 进来,最老的被挤出去 —— 哪怕它还没到 decision_wait。
内存占用大致是 num_traces × 每棵树的字节数。代入 1.1 节的数字:
| 场景 | 每棵树 | × 50,000 | 结论 |
|---|---|---|---|
| 不记 prompt | 4.8 KB | 240 MB | 可接受 |
| 记 prompt | 64 KB | 3.2 GB | 单个 Collector 实例扛不住 |
记 prompt + decision_wait: 120s | 64 KB | 需要把 num_traces 再放大 4 倍 | 12.8 GB,不现实 |
这条路径上的两个官方指标必须上监控:
# 被环形缓冲挤掉、还没做采样决策就没了的 trace 数。非 0 就说明配置不够用
otelcol_processor_tail_sampling_sampling_trace_dropped_too_early
# trace 在缓冲区里待了多久才被移除。看它的 p1 分位 ——
# 如果 p1 已经接近 decision_wait,说明流量再涨一点就会开始丢
otelcol_processor_tail_sampling_sampling_trace_removal_age